/* eslint-disable no-restricted-syntax */
import * as THREE from 'three';

import { useContext, useEffect } from 'react';
import CameraControls from 'camera-controls'; // A camera control for three.js, similar to THREE.OrbitControls yet supports smooth transitions and more features.
import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer';

import { TransformControls } from 'three/examples/jsm/controls/TransformControls';
import { useDispatch } from 'react-redux';
import { drawCamera, drawSceneBox } from './drawing';

import { CameraHelper } from '../SidePanel/CameraPanel/CameraHelper';
import SceneNode from '../../SceneNode';
import { WebSocketContext } from '../WebSocket/WebSocket';
import { subscribe_to_changes } from '../../subscriber';

const msgpack = require('msgpack-lite'); // Fast Pure JavaScript MessagePack Encoder and Decoder
// MessagePack is an efficient binary serialization format. It lets you exchange data among multiple languages like JSON. But it's faster and smaller.

const SCENE_BOX_NAME = 'Scene Box';
const CAMERAS_NAME = 'Training Cameras';

export function get_scene_tree() {
  const scene = new THREE.Scene(); // A scene is a container for all objects, cameras and lights in a 3D scene.
  const sceneTree = new SceneNode(scene);

  const dispatch = useDispatch();

  // Main camera
  const main_camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000); // degrees, aspect ratio, near, far // degree is the vertical field of view // aspect ratio is the ratio of the width of the camera to the height of the camera // near is the distance from the camera to the near clipping plane // far is the distance from the camera to the far clipping plane
  main_camera.position.x = 0.7;
  main_camera.position.y = -0.7;
  main_camera.position.z = 0.3;
  main_camera.up = new THREE.Vector3(0, 0, 1);
  sceneTree.set_object_from_path(['Cameras', 'Main Camera'], main_camera); // the first argument is the path to the camera // the second argument is the camera object // Cameras is the name of the folder that contains all the cameras // Main Camera is the name of the camera

  sceneTree.metadata.camera = main_camera; // set the main camera as the default camera

  // Render camera
  const render_camera = main_camera.clone(); 
  const render_camera_helper = new CameraHelper(render_camera, '#4eb570'); // create a camera helper for the render camera // the first argument is the camera object // the second argument is the color of the camera helper
  render_camera_helper.set_visibility(false); // set the visibility of the camera helper to false
  sceneTree.set_object_from_path(['Cameras', 'Render Camera'], render_camera); 
  sceneTree.set_object_from_path(
    ['Cameras', 'Render Camera', 'Helper'],
    render_camera_helper,
  );

  // Renderer
  const renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true,
  }); // antialias is a boolean value that determines whether the renderer should perform antialiasing // alpha is a boolean value that determines whether the renderer should perform alpha blending
  renderer.setPixelRatio(window.devicePixelRatio); // set the pixel ratio of the renderer // window.devicePixelRatio is the ratio between physical pixels and device-independent pixels
  sceneTree.metadata.renderer = renderer; // set the renderer as the default renderer

  const labelRenderer = new CSS2DRenderer(); // create a label renderer // label renderer is used to render labels // labels are used to display the name of the object
  labelRenderer.setSize(window.innerWidth, window.innerHeight); // set the size of the label renderer // window.innerWidth is the width of the window // window.innerHeight is the height of the window
  labelRenderer.domElement.style.position = 'absolute'; // set the position of the label renderer
  labelRenderer.domElement.style.top = '0px'; // set the top position of the label renderer
  document.body.appendChild(labelRenderer.domElement); // add the label renderer to the body of the document
  sceneTree.metadata.labelRenderer = labelRenderer; // set the label renderer as the default label renderer

  // Camera Controls
  CameraControls.install({ THREE }); // install the camera controls

  const camera_controls = new CameraControls(main_camera, renderer.domElement); // create a camera controls // the first argument is the camera object // the second argument is the dom element
  camera_controls.azimuthRotateSpeed = 0.3; // set the azimuth rotate speed of the camera controls // azimuth rotate speed is the speed of the camera controls when rotating around the z axis
  camera_controls.polarRotateSpeed = 0.3; // set the polar rotate speed of the camera controls // polar rotate speed is the speed of the camera controls when rotating around the x axis
  camera_controls.minDistance = 0.3; // set the minimum distance of the camera controls
  camera_controls.maxDistance = 20; // set the maximum distance of the camera controls

  camera_controls.dollySpeed = 0.1; // set the dolly speed of the camera controls // dolly speed is the speed of the camera controls when zooming in and out
  camera_controls.saveState(); // save the state of the camera controls

  const keyMap = []; // create a key map
  const moveSpeed = 0.02; // set the move speed of the camera controls // move speed is the speed of the camera controls when moving forward, backward, left, and right

  function moveCamera() {
    if (keyMap.ArrowLeft === true) {
      camera_controls.rotate(-0.02, 0, true);
    }
    if (keyMap.ArrowRight === true) {
      camera_controls.rotate(0.02, 0, true);
    }
    if (keyMap.ArrowUp === true) {
      camera_controls.rotate(0, -0.01, true);
    }
    if (keyMap.ArrowDown === true) {
      camera_controls.rotate(0, 0.01, true);
    }
    if (keyMap.KeyD === true) {
      camera_controls.truck(moveSpeed, 0, true);
    }
    if (keyMap.KeyA === true) {
      camera_controls.truck(-moveSpeed, 0, true);
    }
    if (keyMap.KeyW === true) {
      camera_controls.forward(moveSpeed, true);
    }
    if (keyMap.KeyS === true) {
      camera_controls.forward(-moveSpeed, true);
    }
    if (keyMap.KeyQ === true) {
      camera_controls.truck(0, moveSpeed, true);
    }
    if (keyMap.KeyE === true) {
      camera_controls.truck(0, -moveSpeed, true);
    }
  }

  function onKeyUp(event) {
    const keyCode = event.code;
    keyMap[keyCode] = false;
  }
  function onKeyDown(event) {
    const keyCode = event.code; // get the key code of the key that is pressed
    keyMap[keyCode] = true; // set the value of the key code in the key map to true
  }

  window.addEventListener('keydown', onKeyDown, true); 
  window.addEventListener('keyup', onKeyUp, true);

  window.addEventListener('keydown', (event) => {
    if (event.code === 'Space') { // if the key code of the key that is pressed is Space
      camera_controls.setTarget(0, 0, 0); // set the target of the camera controls to the origin
    }
  });

  sceneTree.metadata.camera_controls = camera_controls; // set the camera controls as the default camera controls
  sceneTree.metadata.moveCamera = moveCamera; // set the move camera function as the default move camera function

  // Transform Controls
  const transform_controls = new TransformControls( // Transform controls is used to transform the object 
    main_camera,
    renderer.domElement,
  );
  sceneTree.set_object_from_path(['Transform Controls'], transform_controls);
  transform_controls.addEventListener('dragging-changed', (event) => {
    // turn off the camera controls while transforming an object
    camera_controls.enabled = !event.value;
  });

  // if you drag the screen when the render camera is shown,
  // then snap back to the main camera
  // eslint-disable-next-line no-unused-vars
  camera_controls.addEventListener('change', (event) => {
    if (sceneTree.metadata.camera === render_camera) {
      dispatch({
        type: 'write',
        path: 'renderingState/camera_choice',
        data: 'Main Camera',
      });
    }
    // transform_controls.detach();
  });

  // Axes
  const axes = new THREE.AxesHelper(5); // create an axes helper // axes helper is used to display the axes of the object // the argument is the size of the axes helper, not the dimensions of the axes helper
  sceneTree.set_object_from_path(['Axes'], axes);

  // Grid
  const grid = new THREE.GridHelper(20, 20); // create a grid helper // grid helper is used to display the grid
  grid.rotateX(Math.PI / 2); // rotated to xy plane
  sceneTree.set_object_from_path(['Grid'], grid);

  // Lights
  const color = 0xffffff;
  const intensity = 1;
  const light = new THREE.AmbientLight(color, intensity); // create an ambient light // ambient light is used to light up the scene
  sceneTree.set_object_from_path(['Light'], light);

  // draw scene box
  const selector_fn_scene_box = (state) => {
    return state.sceneState.sceneBox;
  };
  const fn_value_scene_box = (previous, current) => {
    if (current !== null) {
      const line = drawSceneBox(current); // get the line of the scene box 
      sceneTree.set_object_from_path([SCENE_BOX_NAME], line);
    } else {
      sceneTree.delete([SCENE_BOX_NAME]);
    }
  };
  subscribe_to_changes(selector_fn_scene_box, fn_value_scene_box); // if the scene box changes, then draw the scene box

  // draw camera
  // NOTE: this has some issues right now! it won't
  // update the camera on an individual change w/o deleting first
  const selector_fn_cameras = (state) => {
    return state.sceneState.cameras;
  };
  const fn_value_cameras = (previous, current) => {
    if (current !== null) {
      let prev = new Set();
      if (previous !== null) {
        prev = new Set(Object.keys(previous));
      }
      const curr = new Set(Object.keys(current));
      // valid if in current but not previous
      // invalid if in previous but not current
      for (const key of curr) {
        // valid so draw
        if (!prev.has(key)) {
          // keys_valid.push(key);
          const json = current[key];
          const camera = drawCamera(json);
          sceneTree.set_object_from_path([CAMERAS_NAME, key], camera);
        }
      }
      for (const key of prev) {
        // invalid so delete
        if (!curr.has(key) || current[key] === null) {
          // keys_invalid.push(key);
          sceneTree.delete([CAMERAS_NAME, key]);
        }
      }
    } else {
      sceneTree.delete([CAMERAS_NAME]);
    }
  };
  subscribe_to_changes(selector_fn_cameras, fn_value_cameras);

  return sceneTree;
}

// manages setting up the scene and other logic for keeping state in sync with the server
export function SceneTreeWebSocketListener() {
  const socket = useContext(WebSocketContext).socket; // useContext(): Accepts a context object
  const dispatch = useDispatch();  // useDispatch(): Returns a reference to the dispatch function from the Redux store

  useEffect(() => {
    socket.addEventListener('message', (originalCmd) => {
      // set the remote description when the offer is received
      const cmd = msgpack.decode(new Uint8Array(originalCmd.data));
      if (cmd.type === 'write') {
        // write to the store
        dispatch({
          type: 'write',
          path: cmd.path,
          data: cmd.data,
        });
      }
    });
  }, [socket]); // dependency to call this whenever the websocket changes
}
